﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace DigitClassification
{
    public class PerceptronClassifier : BaseClassifier
    {
        public PerceptronClassifier(string dataFile, string labelsFile)
        {
            //Build training data
            Training = GetDigitData(dataFile, labelsFile);
        }

        private CellWeights[] cellWeightsArr = new CellWeights[10];
        private int epochCount;

        public void ReCalcWeights(int limit)
        {
            epochCount = 0;

            //init weights
            for (int i = 0; i < cellWeightsArr.Length; i++)
                cellWeightsArr[i] = new CellWeights(Dim);

            for (int i = 0; i < limit; i++)
            {
                var errorCount = RunEpoch();
                if (errorCount == 0)
                    break;
            }
        }

        public int RunEpoch()
        {
            epochCount++;

            var errorCount = 0;
            foreach (var item in Training)
            {
                var maxArgDigits = GetMaxArgDigit(item);

                //maxArgDigit is correct - continue to next example
                if (maxArgDigits.Count == 1 && maxArgDigits[0] == item.Label)
                    continue; 

                if(!maxArgDigits.Contains(item.Label))
                    ChangeWeights(cellWeightsArr[item.Label], item, 1); //w_correctY = w_correctY + f

                maxArgDigits.Remove(item.Label);

                //maxArgDigit is wrong - need to fix weights according to this example:
                foreach (var digit in maxArgDigits)
                    ChangeWeights(cellWeightsArr[digit], item, -1); //w_maxY = w_maxY - f    
                
                errorCount++;
            }
            return errorCount;
        }

        private List<int> GetMaxArgDigit(DigitData item)
        {
            var maxScore = double.MinValue;
            int maxArgDigit = -1;

            var scores = new double[cellWeightsArr.Length];
            for (int digit = 0; digit < cellWeightsArr.Length; digit++)
            {
                var score = GetScore(item, cellWeightsArr[digit]);
                scores[digit] = score;
                if (score > maxScore)
                {
                    maxScore = score;
                    maxArgDigit = digit;
                }
            }
            return new List<int>() { maxArgDigit };

            //var digits = new List<int>();
            //for (int i = 0; i < cellWeightsArr.Length; i++)
            //{
            //    if (scores[i].Equals(maxScore))
            //        digits.Add(i);
            //}
            //return digits;
        }

        private double GetScore(DigitData item, CellWeights cellWeights)
        {
            double score = 0;
            for (int i = 0; i < Dim; i++)
            {
                for (int j = 0; j < Dim; j++)
                {
                    var fij = IsDark(item.Attributes[i, j]) ? 1 : 0;
                    score += cellWeights.Weights[i, j] * fij;
                }
            }

            return score;
        }

        private void ChangeWeights(CellWeights cellWeights, DigitData item, int delta)
        {
            double learningRate = 1;//(double) 1/epochCount;
            for (int i = 0; i < Dim; i++)
            {
                for (int j = 0; j < Dim; j++)
                {
                    var fij = IsDark(item.Attributes[i, j]) ? 1 : 0;
                    cellWeights.Weights[i, j] += delta*fij*learningRate;
                }
            }
        }

        public double Classify(string sourceName, string dataFile, string labelFile)
        {
            var testData = GetDigitData(dataFile, labelFile);
            var errorCount = 0;

            foreach (var item in testData)
            {
                var classifyDigits = GetMaxArgDigit(item);
                var correctDigit = item.Label;

                if (classifyDigits.Count == 1 && classifyDigits[0] == correctDigit)
                    continue;

                errorCount++;
            }

            var errorRate = (double)errorCount / testData.Count * 100;
            Console.WriteLine(@"total error on {0} using {1} epochs is: {2}/{3} ({4}%)",
                sourceName, epochCount, errorCount, testData.Count, Math.Round(errorRate, 2));

            return errorRate;
        }
    }

    /// <summary>
    /// keep weight for each cell
    /// </summary>
    public class CellWeights
    {
        public double[,] Weights;

        public CellWeights(int dim)
        {
            Weights = new double[dim, dim];
        }

        public override string ToString()
        {
            string s = string.Empty;

            var dim = Math.Sqrt(Weights.Length);
            for (int i = 0; i < dim; i++)
            {
                for (int j = 0; j < dim; j++)
                    s += Weights[i, j]+",";

                s += Environment.NewLine;
            }

            return s;
        }
    }
}